﻿using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.CodeDom.Compiler;
using Microsoft.CSharp;

namespace LongNet.Util
{
    /// <summary>
    /// A smart word filter.
    /// </summary>
    public class SmartFilter : ISmartFilter
    {
        #region Fields

        private ISmartFilter filter = null;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a <see cref="SmartFilter"/> object.
        /// </summary>
        public SmartFilter()
        {
        }

        /// <summary>
        /// Initializes a <see cref="SmartFilter"/> object with the provided word 
        /// dictionary file(UTF8 Encoding).
        /// </summary>
        /// <param name="dictionaryFile">
        /// A word dictionary file(UTF8 Encoding) to load from. 
        /// Each item in the file is separated with a new line, formated as "key|value".
        /// </param>
        public SmartFilter(string dictionaryFile)
        {
            Initialize(dictionaryFile);
        }

        /// <summary>
        /// Initializes a <see cref="SmartFilter"/> object with the provided word dictionary 
        /// which is a KeyValuePair Collection.
        /// </summary>
        /// <param name="dictionary">A word dictionary which is a KeyValuePair Collection.</param>
        public SmartFilter(IEnumerable<KeyValuePair<string, string>> dictionary)
        {
            Initialize(dictionary);
        }

        #endregion

        #region Public Method

        /// <summary>
        /// Replaces all occurrences of the specified key of the word dictionary in original, 
        /// with the specified value of the word dictionary.
        /// </summary>
        /// <param name="original">The string to be filter.</param>
        /// <returns>A filtered string.</returns>
        public string Filter(string original)
        {
            if (string.IsNullOrEmpty(original))
                return string.Empty;

            if (filter == null)
                throw new ArgumentNullException("filter", "Initialize the filter with a word dictionary first.");

            return filter.Filter(original);
        }

        /// <summary>
        /// Initialize the filter with a word dictionary file.
        /// </summary>
        /// <param name="dictionaryFile">
        /// A word dictionary file(UTF8 Encoding) to load from. 
        /// Each item in the file is separated with a new line, formated as "key|value".
        /// </param>
        public void Initialize(string dictionaryFile)
        {
            Initialize(dictionaryFile, null);
        }

        /// <summary>
        /// Initialize the filter with a word dictionary.
        /// </summary>
        /// <param name="dictionaryFile">
        /// A word dictionary file(UTF8 Encoding) to load from. 
        /// Each item in the file is separated with a new line, formated as "key|value".
        /// </param>
        /// <param name="replacement">
        /// If you want to replace all keys in the dictionary with the same string, 
        /// just set it to what(e.g. "*") you wanted, null otherwise.
        /// </param>
        public void Initialize(string dictionaryFile, string replacement)
        {
            var dict = SmartFilter.LoadDictionary(dictionaryFile, replacement);
            if (dict.Count == 0)
                throw new Exception(string.Format("The dictionaryFile \"{0}\" has no words.", dictionaryFile));

            Initialize(dict);
        }

        /// <summary>
        /// Initialize the filter with a word dictionary.
        /// </summary>
        /// <param name="dictionary">A word dictionary which is a KeyValuePair Collection.</param>
        public void Initialize(IEnumerable<KeyValuePair<string, string>> dictionary)
        {
            var compiler = new CSharpCodeProvider();
            var options = new CompilerParameters();

            // set compiler options
            options.GenerateExecutable = false;
            options.GenerateInMemory = true;
            options.ReferencedAssemblies.Add("System.dll");
            options.ReferencedAssemblies.Add(this.GetType().Assembly.Location);

            #region builds source code

            var dict = new Dictionary<string, string>();
            foreach (var pair in dictionary)
            {
                if (!string.IsNullOrEmpty(pair.Key) && pair.Value != null)
                {
                    dict.Add(pair.Key, pair.Value);
                }
            }
            // sortes the dictionary
            var sorted = from pair in dict orderby pair.Key.Length descending select pair;

            StringBuilder sb = new StringBuilder();
            sb.Append(@"
using System;   
using System.Collections.Generic;

public class SmartFilter : " + this.GetType().Namespace + @".ISmartFilter
{   
    public string Filter(string original)
	{
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
		for (int i = 0, l = original.Length; i < l; ++i)
		{");
            bool isFirst = true;
            foreach (var kv in sorted)
            {
                int len = kv.Key.Length;

                if (isFirst == false) sb.Append("else ");

                sb.Append("\nif (");
                for (int i = 0; i < len; i++)
                {
                    if (i > 0)
                        sb.Append(" &&");
                    else
                        sb.Append("(l - i >= ").Append(len).Append(") &&");

                    sb.Append(" original[i").Append(" + ").Append(i).Append("] == '").Append(kv.Key[i]).Append("'");
                }
                sb.Append(")");

                sb.Append("\n{");
                sb.Append("\nsb.Append(\"").Append(kv.Value).Append("\");");
                if (len > 1) sb.Append("\ni += ").Append(len - 1).Append(";");
                sb.Append("\n}");

                isFirst = false;
            }
            sb.Append(@"
			else sb.Append(original[i]);
		}
		return sb.ToString();
    }

    public void Initialize(string dictionaryFile){}
    public void Initialize(string dictionaryFile, string replacement){}
    public void Initialize(IEnumerable<KeyValuePair<string, string>> dictionary){}
}");

            #endregion

            // compiles source code
            var result = compiler.CompileAssemblyFromSource(options, sb.ToString());

            if (result.Errors.HasErrors)
                throw new SystemException("Fail to compile source code.\n" + result.Errors[0]);

            // print source code & errors
            //Console.WriteLine(sb.ToString());
            //foreach (var error in result.Errors) Console.WriteLine(error);

            // if compiles successfully
            if (result.CompiledAssembly != null)
            {
                Type type = result.CompiledAssembly.GetType("SmartFilter");
                try
                {
                    if (type != null)
                        filter = Activator.CreateInstance(type) as ISmartFilter;

                    if (filter == null)
                        throw new SystemException("Fail to initialize filter.");
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
        }

        #endregion

        #region Util Members

        /// <summary>
        /// Loads A word dictionary file(UTF8 Encoding) for <see cref="SmartFilter"/>.
        /// </summary>
        /// <param name="dictionaryFile">
        /// A word dictionary file(UTF8 Encoding) to load from. 
        /// Each item in the file is separated with a new line, formated as "key|value".
        /// </param>
        /// <param name="replacement">
        /// If you want to replace all keys in the dictionary with the same string, 
        /// just set it to what(e.g. "*") you want, null otherwise.
        /// </param>
        /// <returns>A dictionary.</returns>
        public static Dictionary<string, string> LoadDictionary(string dictionaryFile, string replacement)
        {
            if (dictionaryFile == null)
                throw new ArgumentNullException("dictionaryFile");
            if (!File.Exists(dictionaryFile))
                throw new FileNotFoundException(string.Format("The dictionaryFile \"{0}\" is not exists.", dictionaryFile));

            string text = null;
            Dictionary<string, string> dict = new Dictionary<string, string>();

            using (var fs = File.OpenRead(dictionaryFile))
            {
                using (var sr = new StreamReader(fs, Encoding.UTF8))
                {
                    text = sr.ReadToEnd();
                    sr.Close();
                    fs.Close();
                }
            }

            if (!text.IsNullOrTrimedEmpty())
            {
                string[] pairs = text.Split('\n');
                foreach (var p in pairs)
                {
                    if (p.IsNullOrEmpty())
                        continue;

                    string pair = p.Trim().Trim('\n', '\r');

                    string[] keyValue = pair.Split('|');
                    KeyValuePair<string, string> tempPair;
                    if (replacement == null)
                    {
                        if (keyValue.Length > 1)
                            tempPair = new KeyValuePair<string, string>(keyValue[0], keyValue[1]);
                        else
                            tempPair = new KeyValuePair<string, string>(pair, string.Empty);
                    }
                    else
                    {
                        if (keyValue.Length > 1)
                            tempPair = new KeyValuePair<string, string>(keyValue[0], replacement);
                        else
                            tempPair = new KeyValuePair<string, string>(pair, replacement);
                    }

                    if (!dict.ContainsKey(tempPair.Key))
                        dict.Add(tempPair.Key, tempPair.Value);
                }
            }

            return dict;
        }


        /// <summary>
        /// Replaces the keys to the specified replacement in the text, ignore case.
        /// </summary>
        /// <param name="text">A text string.</param>
        /// <param name="format">The format for key, "{0}" e.g.</param>
        /// <param name="keys">The keys array.</param>
        /// <returns>The resolved text string.</returns>
        public static string FormatKeys(string text, string format, params string[] keys)
        {
            return FormatKeys(text, format, true, keys);
        }

        /// <summary>
        /// Replaces the keys to the specified replacement in the text.
        /// </summary>
        /// <param name="text">A text string.</param>
        /// <param name="format">The format for key, "{0}" e.g.</param>
        /// <param name="ignoreCase">Whether ignore case or not.</param>
        /// <param name="keys">The keys array.</param>
        /// <returns>The resolved text string.</returns>
        public static string FormatKeys(string text, string format, bool ignoreCase, params string[] keys)
        {
            if (text == null || text.Length == 0)
                return string.Empty;

            if (keys == null || keys.Length == 0)
                return text;

            List<string> list = new List<string>();
            foreach (var k in keys)
            {
                if (k != null)
                {
                    string s = k.Trim();
                    if (s.Length > 0) list.Add(k.Trim());
                }
            }

            if (list.Count == 0)
                return text;

            // the longest keyword will be matched first
            var sorted = from p in list orderby p.Length descending select p;

            var buider = new StringBuilder();
            if (ignoreCase == false)
            {
                for (int i = 0, l = text.Length; i < l; ++i)
                {
                    int j, len = 0;
                    bool found = true;
                    foreach (string key in sorted)
                    {
                        found = true;
                        len = key.Length;

                        if (l - i >= len)
                        {
                            var word = new char[len];
                            for (j = 0; j < len; ++j)
                            {
                                word[j] = text[i + j];
                                if (word[j] != key[j])
                                {
                                    found = false;
                                    break;
                                }
                            }

                            if (found)
                            {
                                if (len > 1)
                                    i += len - 1;

                                buider.AppendFormat(format, new string(word));

                                break;
                            }
                        }
                        else
                        {
                            found = false;
                        }
                    }

                    if (found == false)
                        buider.Append(text[i]);
                }
            }
            else
            {
                for (int i = 0, l = text.Length; i < l; ++i)
                {
                    int j, len = 0;
                    bool found = true;
                    foreach (string key in sorted)
                    {
                        found = true;
                        len = key.Length;

                        if (l - i >= len)
                        {
                            var word = new char[len];
                            for (j = 0; j < len; ++j)
                            {
                                word[j] = text[i + j];
                                if (char.ToLower(word[j]) != char.ToLower(key[j]))
                                {
                                    found = false;
                                    break;
                                }
                            }

                            if (found)
                            {
                                if (len > 1)
                                    i += len - 1;

                                buider.AppendFormat(format, new string(word));

                                break;
                            }
                        }
                        else
                        {
                            found = false;
                        }
                    }

                    if (found == false)
                        buider.Append(text[i]);
                }
            }

            return buider.ToString();
        }

        /// <summary>
        /// Replaces the keys to the specified replacement in the text, ignore case.
        /// </summary>
        /// <param name="text">A text string.</param>
        /// <param name="replacement">The replacement of key.</param>
        /// <param name="keys">The keys array.</param>
        /// <returns>The resolved text string.</returns>
        public static string ReplaceKeys(string text, string replacement, params string[] keys)
        {
            return ReplaceKeys(text, replacement, true, keys);
        }

        /// <summary>
        /// Replaces the keys to the specified replacement in the text.
        /// </summary>
        /// <param name="text">A text string.</param>
        /// <param name="replacement">The replacement of key.</param>
        /// <param name="ignoreCase">Whether ignore case or not.</param>
        /// <param name="keys">The keys array.</param>
        /// <returns>The resolved text string.</returns>
        public static string ReplaceKeys(string text, string replacement, bool ignoreCase, params string[] keys)
        {
            if (text == null || text.Length == 0)
                return string.Empty;

            if (keys == null || keys.Length == 0)
                return text;

            List<string> list = new List<string>();
            foreach (var k in keys)
            {
                if (k != null)
                {
                    string s = k.Trim();
                    if (s.Length > 0) list.Add(k.Trim());
                }
            }

            if (list.Count == 0)
                return text;

            // the longest keyword will be matched first
            var sorted = from p in list orderby p.Length descending select p;

            var buider = new StringBuilder();
            if (ignoreCase == false)
            {
                for (int i = 0, l = text.Length; i < l; ++i)
                {
                    int j, len = 0;
                    bool found = true;
                    foreach (string key in sorted)
                    {
                        found = true;
                        len = key.Length;

                        if (l - i >= len)
                        {
                            for (j = 0; j < len; ++j)
                            {
                                if (text[i + j] != key[j])
                                {
                                    found = false;
                                    break;
                                }
                            }

                            if (found)
                            {
                                if (len > 1)
                                    i += len - 1;

                                buider.Append(replacement);

                                break;
                            }
                        }
                    }

                    if (found == false)
                        buider.Append(text[i]);
                }
            }
            else
            {
                for (int i = 0, l = text.Length; i < l; ++i)
                {
                    int j, len = 0;
                    bool found = true;
                    foreach (string key in sorted)
                    {
                        found = true;
                        len = key.Length;

                        if (l - i >= len)
                        {
                            for (j = 0; j < len; ++j)
                            {
                                if (char.ToLower(text[i + j]) != char.ToLower(key[j]))
                                {
                                    found = false;
                                    break;
                                }
                            }

                            if (found)
                            {
                                if (len > 1)
                                    i += len - 1;

                                buider.Append(replacement);

                                break;
                            }
                        }
                    }

                    if (found == false)
                        buider.Append(text[i]);
                }
            }

            return buider.ToString();
        }

        #endregion
    }

    /// <summary>
    /// Smart word filter interface.
    /// </summary>
    public interface ISmartFilter
    {
        /// <summary>
        /// Replaces all occurrences of the specified key of the word dictionary in original, 
        /// with the specified value of the word dictionary.
        /// </summary>
        /// <param name="original">The string to be filter.</param>
        /// <returns>A filtered string.</returns>
        string Filter(string original);

        /// <summary>
        /// Initialize the filter with a word dictionary file.
        /// </summary>
        /// <param name="dictionaryFile">
        /// A word dictionary file(UTF8 Encoding) to load from. 
        /// Each item in the file is separated with a new line, formated as "key|value".
        /// </param>
        void Initialize(string dictionaryFile);

        /// <summary>
        /// Initialize the filter with a word dictionary.
        /// </summary>
        /// <param name="dictionaryFile">
        /// A word dictionary file(UTF8 Encoding) to load from. 
        /// Each item in the file is separated with a new line, formated as "key|value".
        /// </param>
        /// <param name="replacement">
        /// If you want to replace all keys in the dictionary with the same string, 
        /// just set it to what(e.g. "*") you wanted, null otherwise.
        /// </param>
        void Initialize(string dictionaryFile, string replacement);

        /// <summary>
        /// Initialize the filter with a word dictionary.
        /// </summary>
        /// <param name="dictionary">A word dictionary which is a KeyValuePair Collection.</param>
        void Initialize(IEnumerable<KeyValuePair<string, string>> dictionary);
    }
}
